/**
 * Copyright (c) 2008, Mu Dynamics.
 *  All rights reserved.
 *  
 *   Redistribution and use in source and binary forms, with or without modification, 
 *   are permitted provided that the following conditions are met:
 *   
 *  - Redistributions of source code must retain the above copyright notice, 
 *     this list of conditions and the following disclaimer.
 *  - Redistributions in binary form must reproduce the above copyright notice, 
 *     this list of conditions and the following disclaimer in the documentation and/or 
 *     other materials provided with the distribution.
 *  - Neither the name of the "Mu Dynamics" nor the names of its contributors may be used 
 *     to endorse  or promote products derived from this software without specific prior 
 *     written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
 * THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package com.mu.jacob.core.generator;

import static com.google.inject.matcher.Matchers.annotatedWith;
import static com.google.inject.matcher.Matchers.inPackage;
import static com.google.inject.matcher.Matchers.subclassesOf;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.TypeParameterElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.ErrorType;
import javax.lang.model.type.ExecutableType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.NullType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeVariable;
import javax.lang.model.type.WildcardType;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Types;

import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.apache.log4j.Logger;

import com.google.inject.Binder;
import com.google.inject.Module;
import com.mu.jacob.core.model.Model;
import com.mu.jacob.core.model.decorator.DefaultDecorator;

/**
 * DecoratedModule is the main decorator factory.
 * It defines interceptors for calls to Java compiler
 * classes and methods to decorate this with 
 * custom decorators.
 * @author Adam Smyczek
 */
public class DecoratorModule implements Module {

	/* Jacob config, not injected */
	private Config config = null;
	
	/**
	 * Set config after initialization
	 * @param config
	 */
	public void setConfig(Config config) {
		this.config = config;
	}
	
	/**
	 * Modules configure method binds
	 * interceptors for Java compiler api calls.
	 */
	public void configure(Binder binder) {
		
		// Intercept model class
		binder.bindInterceptor(
				subclassesOf(Model.class),
				annotatedWith(Decorate.class),
				interceptor);
		
		// Intercept decorator classes in decorator package
		binder.bindInterceptor(
				inPackage(DefaultDecorator.class.getPackage()),
				annotatedWith(Decorate.class),
				interceptor);
	}
	
	/**
	 * Method interceptor implementation
	 */
	public MethodInterceptor interceptor = new MethodInterceptor() {

		/**
		 * Method invocation redirects result to decorator implementation
		 */
		public Object invoke(MethodInvocation method) throws Throwable {
			Object result = method.proceed();
			// Don't decorate if result is a DefaultDecorator already
			return (result instanceof DefaultDecorator)? result : decorateImpl(result);
		}
	
		/**
		 * Main decorator method
		 * @param resultObject
		 * @return decorated object, list or set
		 */
		@SuppressWarnings("unchecked")
		private Object decorateImpl(Object resultObject) {
			// is a collection
			if (resultObject instanceof Collection) {
				try {
					// TODO: make the collection decoration more general
					Collection collection = null;
					if (resultObject instanceof Set) {
						collection = new LinkedHashSet();
					} else if (resultObject instanceof List) {
						collection = new ArrayList();
					} else {
						collection = (Collection)resultObject.getClass().newInstance();
					}
					Iterator iter = ((Collection)resultObject).iterator();
					while (iter.hasNext()) {
						collection.add(decorateImpl(iter.next()));
					}
					return collection;
				} catch (Exception e) {
					logger.error("Unable to instantiate collection " + resultObject.getClass(), e);
				}
			} else if (resultObject instanceof Map) {
				Map map = new HashMap();
				Map resultMap = (Map)resultObject;
				for (Object key : resultMap.keySet()) {
					map.put(decorateImpl(key), decorateImpl(resultMap.get(key)));
				}
				return map;
			}
		
			// decorate object
			return decorateInstance(resultObject);
		}

		/**
		 * Decorate single instance
		 * @param decorated
		 * @return decorated instance if the instance is a 
		 * Java compiler interface or the same object.
		 */
		@SuppressWarnings("unchecked")
		protected Object decorateInstance(Object decorated) {
			Class clazz = classFor(decorated);
			if (clazz != null) {
				DefaultDecorator decorator = (DefaultDecorator)config.getInstance(classFor(decorated));
				if (decorator == null) {
					throw new GeneratorException("No decorator for " + decorated + " found!");
				}
				decorator.initialize(decorated);
				return decorator;
			}
			
			return decorated;
		}
		
		/**
		 * Mapping between compiler classes and exposed interfaces
		 */
		private Class classFor(final Object decorated) {
			// Type mirrors
			if (decorated instanceof TypeMirror) {
				switch (((TypeMirror)decorated).getKind()) {
					case VOID : return NoType.class;
					case NULL : return NullType.class;
					case NONE : return TypeMirror.class;
					case ARRAY : return ArrayType.class;
					case DECLARED : return DeclaredType.class;
					case ERROR:	return ErrorType.class;
					case TYPEVAR : return TypeVariable.class;
					case WILDCARD : return WildcardType.class;
					case PACKAGE : return NoType.class;
					case EXECUTABLE : return ExecutableType.class;
					default : return PrimitiveType.class;
				}
			}
			
			// Elements
			if (decorated instanceof Element) {
				if (isAssignable(PackageElement.class, decorated)) { return PackageElement.class; }
				if (isAssignable(TypeElement.class, decorated)) { return TypeElement.class; }
				if (isAssignable(ExecutableElement.class, decorated)) { return ExecutableElement.class; }
				if (isAssignable(TypeParameterElement.class, decorated)) { return TypeParameterElement.class; }
				if (isAssignable(VariableElement.class, decorated)) { return VariableElement.class; }
				// fallback for unknown element
				if (isAssignable(Element.class, decorated)) { return Element.class; }
			}
			
			// Annotations interfaces
			if (decorated instanceof AnnotationMirror) { return AnnotationMirror.class; }
			if (decorated instanceof AnnotationValue) { return AnnotationValue.class; }
			
			// Unit classes
			if (decorated instanceof Elements) { return Elements.class; }
			if (decorated instanceof Types) { return Types.class; }
			
			return null;
		}
		
		/**
		 * isAssignableFrom helper
		 */
		@SuppressWarnings("unchecked")
		private boolean isAssignable(Class clazz, Object obj) {
			return clazz.isAssignableFrom(obj.getClass());
		}
		
	};
	
	private final static Logger logger = Logger.getLogger(DecoratorModule.class);
	
}
